S13-05 React-Router
[TOC]
API
React-Router
<BrowserRouter>
:返回:
,使用 HTML5 的history API
来实现路由功能。<HashRouter>
:返回:
,使用URL hash
(即 URL 中的#
字符)来实现路由功能。<Switch>
:返回:
,用于渲染与当前 URL 匹配的第一个<Route>
或<Redirect>
组件。当匹配到第一个符合条件的组件时,<Switch>
将停止匹配并呈现该组件。React Router 6.x中被<Routes>
代替<Routes>
:返回:
,是一种React Router 6.x新的语法,用于定义路由配置。它是一个包含多个<Route>
或<React.Fragment>
元素的容器组件,每个<Route>
元素都表示一个路由规则。<Route>
:返回:
,用于定义路由规则的组件。- 属性
- path:``,表示路由规则的路径。
- element:
,表示匹配到该路由规则时渲染的组件。注意:element的值是一个组件实例,如
<Home/>
<Link>
:返回:
,用于定义导航链接的组件- 属性
- to:
string
,表示链接的目标位置 - replace:
boolean
,表示是否使用替换历史记录而不是添加一个新条目。默认为 false。 - state:
object
,表示要传递给目标位置的状态。这个状态可以在目标位置的 location 对象中访问到。
<NavLink>
:返回:
,用于定义导航链接的组件,它与<Link>
的功能类似,但是在渲染时会自动添加一个 active 类名,以便高亮当前选中的链接。- 属性
- to:
string
,表示链接的目标位置 - style:
({isActive, isPending?}) => {}
,设置样式 - className:
({isActive, isPending?}) => void
,设置class - activeClassName:
string
,表示选中链接时要添加的类名。默认为 active。 - activeStyle:
object
,表示选中链接时要应用的样式 - isActive:
function
,用于自定义激活链接的逻辑
<Navigate>
:返回:
,用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中- 属性
- to:
string
,表示要导航到的路径。 - replace:
boolean
,表示是否使用替换历史记录而不是添加一个新条目。默认为 false。 - state:
object
,表示要传递给目标位置的状态。这个状态可以在目标位置的 location 对象中访问到。
<Outlet>
:返回:
,是一个新的组件,用于在路由组件中渲染子路由。它充当了一个占位符,用于渲染嵌套路由的内容。- useNavigate:
返回:
, - useParams:
返回:
, - useLocation:
返回:
, - useRoutes:
返回:
,
React
- React.lazy():
返回:
,
认识React-Router
认识前端路由
路由其实是网络工程中的一个术语:
在架构一个网络时,非常重要的两个设备就是路由器和交换机。
当然,目前在我们生活中路由器也是越来越被大家所熟知,因为我们生活中都会用到路由器:
事实上,路由器主要维护的是一个映射表;
映射表会决定数据的流向;
路由的概念在软件工程中出现,最早是在后端路由中实现的,原因是web的发展主要经历了这样一些阶段:
后端路由阶段;
前后端分离阶段;
单页面富应用(SPA);
后端路由阶段
早期的网站开发整个HTML页面是由服务器来渲染的.
- 服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示.
但是, 一个网站, 这么多页面服务器如何处理呢?
一个页面有自己对应的网址, 也就是URL;
URL会发送到服务器, 服务器会通过正则对该URL进行匹配, 并且最后交给一个Controller进行处理;
Controller进行各种处理, 最终生成HTML或者数据, 返回给前端.
上面的这种操作, 就是后端路由:
当我们页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户端.
这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化.
后端路由的缺点:
一种情况是整个页面的模块由后端人员来编写和维护的;
另一种情况是前端开发人员如果要开发页面, 需要通过PHP和Java等语言来编写页面代码;
而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情;
前后端分离阶段
前端渲染的理解:
每次请求涉及到的静态资源都会从静态资源服务器获取,这些资源包括HTML+CSS+JS,然后在前端对这些请求回来的资源进行渲染;
需要注意的是,客户端的每一次请求,都会从静态资源服务器请求文件;
同时可以看到,和之前的后端路由不同,这时后端只是负责提供API了;
前后端分离阶段:
随着Ajax的出现, 有了前后端分离的开发模式;
后端只提供API来返回数据,前端通过Ajax获取数据,并且可以通过JavaScript将数据渲染到页面中;
这样做最大的优点就是前后端责任的清晰,后端专注于数据上,前端专注于交互和可视化上;
并且当移动端(iOS/Android)出现后,后端不需要进行任何处理,依然使用之前的一套API即可;
目前比较少的网站采用这种模式开发;
单页面富应用阶段:
其实SPA最主要的特点就是在前后端分离的基础上加了一层前端路由.
也就是前端来维护一套路由规则.
前端路由的核心是什么呢?改变URL,但是页面不进行整体的刷新。
URL的hash
前端路由是如何做到URL和内容进行映射呢?监听URL的改变。
URL的hash
URL的hash也就是锚点(#), 本质上是改变window.location的href属性;
我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;
hash的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个#,显得不像一个真实的路径。
HTML5的History
history接口是HTML5新增的, 它有六种模式改变URL而不刷新页面:
replaceState:替换原来的路径;
pushState:使用新的路径;
popState:路径的回退;
go:向前或向后改变路径;
forward:向前改变路径;
back:向后改变路径;
认识react-router
目前前端流行的三大框架, 都有自己的路由实现:
Angular的ngRouter
React的ReactRouter
Vue的vue-router
React Router在最近两年版本更新的较快,并且在最新的React Router6.x版本中发生了较大的变化。
- 目前React Router6.x已经非常稳定,我们可以放心的使用;
Router-基本使用
安装React Router
安装时,我们选择react-router-dom;
react-router会包含一些react-native的内容,web开发并不需要;
npm install react-router-dom
Router的基本使用
react-router最主要的API是给我们提供的一些组件:
<BrowserRouter>
:返回:
,使用 HTML5 的history API
来实现路由功能。<HashRouter>
:返回:
,使用URL hash
(即 URL 中的#
字符)来实现路由功能。
BrowserRouter或HashRouter
Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件;
BrowserRouter使用history模式;
HashRouter使用hash模式;
// index.js
import { HashRouter } from 'react-router-dom'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
+ <HashRouter>
<App />
+ </HashRouter>
)
路由映射配置
<Switch>
:返回:
,用于渲染与当前 URL 匹配的第一个<Route>
或<Redirect>
组件。当匹配到第一个符合条件的组件时,<Switch>
将停止匹配并呈现该组件。React Router 6.x中被<Routes>
代替<Routes>
:返回:
,是一种React Router 6.x新的语法,用于定义路由配置。它是一个包含多个<Route>
或<React.Fragment>
元素的容器组件,每个<Route>
元素都表示一个路由规则。<Route>
:返回:
,用于定义路由规则的组件。- 属性
- path:``,表示路由规则的路径。
- element:
,表示匹配到该路由规则时渲染的组件。注意:element的值是一个组件实例,如
<Home/>
Routes:包裹所有的Route,在其中匹配一个路由
- Router5.x使用的是Switch组件
Route:Route用于路径的匹配;
path属性:用于设置匹配到的路径;
element属性:设置匹配到路径后,渲染的组件;
- Router5.x使用的是component属性
exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
- Router6.x不再支持该属性
App.jsx
export class App extends PureComponent {
render() {
return (
<div>
<div className="header">Header</div>
<hr />
<div className="content">
+ <Routes>
+ <Route path='/home' element={<Home/>}/>
+ <Route path='/profile' element={<Profile/>}/>
+ </Routes>
</div>
<hr />
<div className="footer">Footer</div>
</div>
)
}
}
路由配置和跳转
<Link>
:返回:
,用于定义导航链接的组件- 属性
- to:
string
,表示链接的目标位置 - replace:
boolean
,表示是否使用替换历史记录而不是添加一个新条目。默认为 false。 - state:
object
,表示要传递给目标位置的状态。这个状态可以在目标位置的 location 对象中访问到。
Link和NavLink:**
通常路径的跳转是使用Link组件,最终会被渲染成a元素;
NavLink是在Link基础之上增加了一些样式属性(后续学习);
to属性:Link中最重要的属性,用于设置跳转到的路径;
NavLink的使用
<NavLink>
:返回:
,用于定义导航链接的组件,它与 <Link>
的功能类似,但是在渲染时会自动添加一个 active 类名,以便高亮当前选中的链接。
- 属性
- to:
string
,表示链接的目标位置 - style:
({isActive, isPending?}) => {}
,设置样式 - className:
({isActive, isPending?}) => void
,设置class - activeClassName:
string
,表示选中链接时要添加的类名。默认为 active。 - activeStyle:
object
,表示选中链接时要应用的样式 - isActive:
function
,用于自定义激活链接的逻辑
需求:路径选中时,对应的a元素变为红色
这个时候,我们要使用NavLink组件来替代Link组件:
style:传入函数,函数接受一个对象,包含isActive属性
js<NavLink to="/detail" style={({ isActive }) => ({ backgroundColor: isActive ? '#ddd' : '' })} > 详情 </NavLink>
className:传入函数,函数接受一个对象,包含isActive属性
js{/* 1. 普通添加class */} <NavLink to="/category" className={({isActive}) => isActive ? 'mractive' : ''}>分类</NavLink> {/* 2. 使用classnames库 */} <NavLink to="/home" className={({isActive}) => classNames({mractive: isActive})}>首页</NavLink>
activeClassName
- 事实上在默认匹配成功时,NavLink就会添加上一个动态的active class;
js<div className="footer"> <NavLink to="/home">首页</NavLink> <NavLink to="/profile">我的</NavLink> </div>
- 所以我们也可以直接编写样式
当然,如果你担心这个class在其他地方被使用了,出现样式的层叠,也可以自定义class
Navigate导航
<Navigate>
:返回:
,用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中
- 属性
- to:
string
,表示要导航到的路径。 - replace:
boolean
,表示是否使用替换历史记录而不是添加一个新条目。默认为 false。 - state:
object
,表示要传递给目标位置的状态。这个状态可以在目标位置的 location 对象中访问到。
Navigate用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中:
我们这里使用这样的一个案例:
用户跳转到Profile界面;
但是在Profile界面有一个isLogin用于记录用户是否登录:
true:那么显示用户的名称;
false:直接重定向到登录界面;
示例:点击登录跳转到Home页
export class Profile extends PureComponent {
constructor() {
super()
this.state = {
+ isLogin: false
}
}
render() {
const { isLogin } = this.state
return (
<div>
<h3>Profile</h3>
{/*
功能:通过isLogin判断是否登录,
如果登录则跳转到首页,
如果没有登录则留在本页,并且在本页提供一个登录按钮,点击实现登录
*/}
{
+ isLogin ? <Navigate to='/home'/> : <button onClick={e => this.setState({isLogin: true})}>登录</button>
}
</div>
)
}
}
示例:匹配到/
时直接跳转到/home
页面
<Routes>
+ <Route path='/' element={<Navigate to='/home'/>} />
<Route path="/home" element={<Home />} />
<Route path="/profile" element={<Profile />} />
<Route path="/detail" element={<Detail />} />
<Route path="/category" element={<Category />} />
</Routes>
Not Found页面配置
如果用户随意输入一个地址,该地址无法匹配,那么在路由匹配的位置将什么内容都不显示。
很多时候,我们希望在这种情况下,让用户看到一个Not Found的页面。
这个过程非常简单:
开发一个Not Found页面;
配置对应的Route,并且*设置path为**即可;
<Routes>
<Route path='/' element={<Navigate to='/home'/>} />
<Route path="/home" element={<Home />} />
<Route path="/profile" element={<Profile />} />
<Route path="/detail" element={<Detail />} />
<Route path="/category" element={<Category />} />
+ <Route path='*' element={<NotFound />} />
</Routes>
Router-路由嵌套
<Outlet>
:返回:
,是一个新的组件,用于在路由组件中渲染子路由。它充当了一个占位符,用于渲染嵌套路由的内容。
在开发中,路由之间是存在嵌套关系的。
这里我们假设Home页面中有两个页面内容:
推荐列表和排行榜列表;
点击不同的链接可以跳转到不同的地方,显示不同的内容;
Outlet 组件用于在父路由元素中作为子路由的占位元素。
App.jsx
<Routes>
<Route path='/' element={<Navigate to='/home'/>} />
+ <Route path="/home" element={<Home />}>
+ <Route path='/home/recommend' element={<HomeRecommend />}/>
+ <Route path='/home/rank' element={<HomeRank />}/>
+ </Route>
<Route path="/profile" element={<Profile />} />
<Route path="/detail" element={<Detail />} />
<Route path="/category" element={<Category />} />
<Route path='*' element={<NotFound />} />
</Routes>
Home.jsx
<Link to='/home/recommend'>推荐榜 </Link>
<Link to='/home/rank'>排行榜</Link>
+ <Outlet />
Router-手动路由跳转
目前我们实现的跳转主要是通过Link或者NavLink进行跳转的,实际上我们也可以通过JavaScript代码进行跳转。
我们知道Navigate组件是可以进行路由跳转的,但是依然是组件的方式。
如果我们希望通过JavaScript代码逻辑进行跳转(比如点击了一个button),那么就需要获取到navigate对象。
在Router6.x版本之后,代码类的API都迁移到了hooks的写法:
如果我们希望进行代码跳转,需要通过useNavigate的Hook获取到navigate对象进行操作;
那么如果是一个函数式组件,我们可以直接调用,但是如果是一个类组件呢?
使用useNavigate的方法:
1、修改成函数组件
import React from 'react'
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom'
import Home from './cpns/Home'
import Profile from './cpns/Profile'
import NotFound from './cpns/NotFound'
+ export function App(props) {
// 1. 调用useNavigate钩子,返回方法
+ const navigate = useNavigate()
return (
<div>
<div className="header">
{/* 2. 使用navigate方法,实现跳转 */}
+ <button onClick={e => navigate('/home')}>首页</button>
+ <button onClick={e => navigate('/profile')}>我的</button>
</div>
<hr />
<div className="content">
<Routes>
<Route path="/" element={<Navigate to="/home" />} />
<Route path="/home" element={<Home />} />
<Route path="/profile" element={<Profile />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
<hr />
<div className="footer">Footer</div>
</div>
)
}
export default App
2、对类组件使用高阶组件包裹
hoc/withRouter.js
import { useNavigate } from "react-router-dom"
function withRouter(Cpn) {
return function(props) {
const navigate = useNavigate()
const router = { navigate }
return <Cpn {...props} router={router}/>
}
}
export default withRouter
App.jsx
import React, { PureComponent } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import Home from './cpns/Home'
import Profile from './cpns/Profile'
import NotFound from './cpns/NotFound'
import withRouter from './hoc/withRouter'
+ export class App extends PureComponent {
render() {
// 2. 解构出router对象中的navigate方法
+ const { router } = this.props
+ const { navigate } = router
return (
<div>
<div className="header">
{/* 3. 使用navigate方法,实现跳转 */}
+ <button onClick={(e) => navigate('/home')}>首页</button>
+ <button onClick={(e) => navigate('/profile')}>我的</button>
</div>
<hr />
<div className="content">
<Routes>
<Route path="/" element={<Navigate to="/home" />} />
<Route path="/home" element={<Home />} />
<Route path="/profile" element={<Profile />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
<hr />
<div className="footer">Footer</div>
</div>
)
}
}
// 1. 使用高阶组件withRouter包裹App
+ export default withRouter(App)
Router-参数传递
传递参数有二种方式:
动态路由参数
查询字符串参数
1、动态路由参数
动态路由的概念指的是路由中的路径并不会固定:
比如/detail的path对应一个组件Detail;
如果我们将path在Route匹配时写成/detail/:id,那么 /detail/abc、/detail/123都可以匹配到该Route,并且进行显示;
这个匹配规则,我们就称之为动态路由;
通常情况下,使用动态路由可以为路由传递参数。
跳转时传递参数
1、路由
<Routes>
<Route path="/" element={<Navigate to="/home" />} />
{/* 1. 路由传参-动态路由 */}
+ <Route path="/home/:id" element={<Home />} />
<Route path="/profile" element={<Profile />} />
<Route path="*" element={<NotFound />} />
</Routes>
2、跳转时传参
{/* 2. 使用navigate方法,实现跳转 */}
+ <button onClick={(e) => navigate('/home/123')}>首页</button>
+ <Link to="/profile/456">我的</Link>
3、由于类组件不能使用useParams,所以必须对该类组件进行增强
import { useNavigate, useParams } from "react-router-dom"
function withRouter(Cpn) {
return function(props) {
// 路由跳转
const navigate = useNavigate()
// 路由传参-动态路由
+ const params = useParams()
const router = { navigate, params }
return <Cpn {...props} router={router}/>
}
}
export default withRouter
4、跳转后获取参数
import React, { PureComponent } from 'react'
import withRouter from '../hoc/withRouter'
export class Home extends PureComponent {
render() {
const { router } = this.props
+ const { params } = router
console.log('Home: ', params)
return (
<div>
<h3>Home</h3>
+ <div>id: {params.id}</div>
</div>
)
}
}
+ export default withRouter(Home)
2、查询字符串参数
- useLocation:``,
- useSearchParams:``,
1、路由
<Routes>
<Route path="/" element={<Navigate to="/home" />} />
{/* 1. 路由传参-动态路由 */}
<Route path="/home/:id" element={<Home />} />
<Route path="/profile" element={<Profile />} />
{/* 2. 路由传参-查询字符串 */}
+ <Route path='/about' element={<About />}/>
<Route path="*" element={<NotFound />} />
</Routes>
2、传递参数
<Link to="/about?name=Tom&age=20">关于</Link>
3、由于类组件不能使用useParams,所以必须对该类组件进行增强
方式一:useSearchParams()
方式二:useLocation()
import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom"
function withRouter(Cpn) {
return function(props) {
// 路由跳转
const navigate = useNavigate()
// 路由传参-动态路由
const params = useParams()
// 路由传参-查询字符串-useSearchParams
+ const [ searchParams ] = useSearchParams()
+ const query = Object.fromEntries(searchParams.entries())
// 路由传参-查询字符串-useLocation
+ const { search } = useLocation()
+ const searchEntries = new URLSearchParams(search)
+ const searchObj = Object.fromEntries(searchEntries)
const router = { navigate, params, query, searchObj }
return <Cpn {...props} router={router}/>
}
}
export default withRouter
4、获取参数
import React, { PureComponent } from 'react'
import withRouter from '../hoc/withRouter'
export class About extends PureComponent {
render() {
const { router } = this.props
+ const { query, searchObj } = router
return (
<div>
<div>About</div>
+ <div>name: {query.name}, age: {query.age}</div>
+ <div>name-{searchObj.name}, age-{searchObj.age}</div>
</div>
)
}
}
+ export default withRouter(About)
Router-配置文件
目前我们所有的路由定义都是直接使用Route组件,并且添加属性来完成的。
但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理:
在早期的时候,Router并且没有提供相关的API,我们需要借助于
react-router-config
完成;在Router6.x中,为我们提供了useRoutes API可以完成相关的配置;
配置过程
1、创建 router/index.js
通过 useRoutes
将配置信息生成为Route组件,导出一个BaseRoute
的函数组件
import { Navigate, useRoutes } from "react-router-dom";
export default function BaseRoute() {
return useRoutes(
[
{ path: '/', element: <Navigate to='/home'/> },
{
path: '/home',
element: <Home />,
children: [
{ path: '/home/recommend', element: <HomeRecommend /> },
{ path: '/home/banner', element: <HomeBanner /> }
]
},
{ path: '/profile', element: <Profile /> },
{ path: '/about', element: <About /> },
{ path: '*', element: <NotFound /> }
]
)
}
2、在App.jsx
中使用该组件
<div className="content">
+ <BaseRoute />
</div>
注意: 这里不能在router/index.js
中导出routes
,然后在App.jsx
中使用useRoutes
生成Route组件,会报错
React Hook "useRoutes" cannot be called in a class component
注意: 在路由配置中如果该路由下面有子路由(children),那么path不能写成/home/:id
这样,只能是/home
3、如果我们对某些组件进行了异步加载(懒加载),那么需要使用Suspense进行包裹,实现分包
懒加载
React.lazy() 是 React 16.6 新增的 API,它实现了一种动态加载组件的方式,可以延迟组件的加载到真正需要渲染的时候再进行加载,从而优化页面加载速度。其语法如下:
const MyComponent = React.lazy(() => import('./MyComponent'));
其中 import()
函数返回一个 Promise 对象,表示 MyComponent 组件的代码将在被第一次渲染时动态地加载。
在使用React.lazy()时,需要注意几个方面:
React.lazy()
只能用于函数式组件,不能用于 class 组件;- 必须使用 React.Suspense 组件来包装懒加载组件,以防止在加载完成之前渲染出错;
- 在开发环境中,React.lazy() 需要与 babel 插件*@babel/plugin-syntax-dynamic-import* 配合使用。
以下是一个示例,展示了如何使用React.lazy()实现懒加载:
import React, { lazy, Suspense } from 'react';
const OtherComponent = lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
在上面的代码中,我们使用 React.lazy()
动态加载了一个组件,然后使用 <Suspense>
组件包裹起来,设置了一个 fallback
属性,表示在组件加载完成前显示的内容,这里设置为 "Loading..."。这样确保了即使组件还未加载完成,也不会影响整个页面的渲染。
另外值得一提的是,React.lazy()并不会影响应用的事件处理逻辑,因为它并不是异步地获取数据,而是关于代码的懒加载。